import {designComponent, iHTMLDivElement, onMounted, PropType, useRefs} from "@peryl/react-compose";
import {eRenderMode, execIndexScript, execMainScript, formatStyleTags, formatVitePublicPath, getGlobalCacheScript, iIndexData, iMicroApp, iMicroMainConfig, iMicroTokenInfo, keepFormattingStyleTags} from '@peryl/micro-web';
import {pathJoin} from "@peryl/utils/pathJoin";
import {microLoadingHook} from "./useMicroLoading";
import {$notice, ApplicationConfigurationProvider} from "plain-design";

/**
 * 使用frame模式挂载子应用，挂载的核心原理是：
 * 1. 创建一个同源的iframe，
 * 2. 创建一个shadow dom，shadow dom的节点克隆于 iframe.contentDocument.documentElement；
 * 4. 由于无法劫持代理 window.document，所以这里是劫持代理 window.document对象内的部分属性；
 * 3. 代理劫持 iframe 的 window.document 的body，head以及documentElement节点，当访问这些节点时返回 shadow dom的body，head，documentElement；所以当初始化脚本执行时，实际上挂载的根节点变成了shadow dom中的body
 * 4. 代理劫持 iframe 的 window.document 的querySelector，querySelectorAll函数，当调用这些函数时调用shadow dom的 documentElement的方法进行查询；
 * 5. 代理劫持 iframe 的 window.history 的pushState以及replaceState方法，子路由变化时通知主应用同步路由
 * @author  韦胜健
 * @date    2024/3/5 16:06
 */
export const FrameModeFrame = designComponent({
  props: {
    indexData: { type: Object as PropType<iIndexData>, required: true },
    microApp: { type: Object as PropType<iMicroApp>, required: true },
    tokenInfo: { type: Object as PropType<iMicroTokenInfo>, required: true },
  },
  setup({ props }) {

    const configuration = ApplicationConfigurationProvider.inject();

    const { microApp } = props;

    const { refs, onRef } = useRefs({ el: iHTMLDivElement });

    const onComplete = (error?: string | null | undefined) => {microLoadingHook.exec({ id: props.indexData.microConfig.id, error });};

    onMounted(async () => {
      try {
        const { appContainer, Win } = await (async () => {

          /*创建iframe，iframe的src指向同源base应用的blank.html，用这个blank.html来加载子应用；因为src必须指向一个同源的地址，不然为空的话会导致子应用无法使用pushState,replaceState*/
          const frame = await new Promise<HTMLIFrameElement>((resolve, reject) => {
            const _frame = document.createElement('iframe') as HTMLIFrameElement;
            _frame.src = pathJoin(microApp.baseRoutePublicPath.get(), 'blank.html');
            _frame.onload = () => {resolve(_frame);};
            _frame.onerror = (e) => (console.error(e), reject(e));
            refs.el!.appendChild(_frame);

            /*解决owner的指向问题*/
            (() => {
              const iframeWindow = _frame.contentWindow as any;

              function patchElementEffect(
                element: (HTMLElement | Node | ShadowRoot) & { _hasPatch?: boolean },
                iframeWindow: Window
              ): void {
                if (element._hasPatch) return;
                try {
                  Object.defineProperties(element, {
                    ownerDocument: {
                      configurable: true,
                      get: () => iframeWindow.document,
                    },
                    _hasPatch: { get: () => true },
                  });
                } catch (error) {
                  console.warn(error);
                }
              }

              const rawGetRootNode = iframeWindow.Node.prototype.getRootNode;
              const rawAppendChild = iframeWindow.Node.prototype.appendChild;
              const rawInsertRule = iframeWindow.Node.prototype.insertBefore;
              iframeWindow.Node.prototype.getRootNode = function (options?: GetRootNodeOptions): Node {
                const rootNode = rawGetRootNode.call(this, options);
                if (rootNode === shadowRoot) return iframeWindow.document;
                else return rootNode;
              };
              iframeWindow.Node.prototype.appendChild = function <T extends Node>(node: T): T {
                const res = rawAppendChild.call(this, node);
                patchElementEffect(node, iframeWindow);
                return res;
              };
              iframeWindow.Node.prototype.insertBefore = function <T extends Node>(node: T, child: Node | null): T {
                const res = rawInsertRule.call(this, node, child);
                patchElementEffect(node, iframeWindow);
                return res;
              };

              iframeWindow.MouseEvent = MouseEvent;
              iframeWindow.KeyboardEvent = KeyboardEvent;
              iframeWindow.FocusEvent = FocusEvent;
              iframeWindow.DragEvent = DragEvent;
              iframeWindow.TransitionEvent = TransitionEvent;
              iframeWindow.WheelEvent = WheelEvent;
              iframeWindow.ClipboardEvent = ClipboardEvent;
              iframeWindow.CompositionEvent = CompositionEvent;
              iframeWindow.TouchEvent = TouchEvent;
            })();
          });

          /*因为要对frame的一些属性做劫持代理，这里保存原始的对象引用*/
          const frameElements = {
            window: frame.contentWindow!,
            document: frame.contentDocument!,
            body: frame.contentDocument!.body!,
            head: frame.contentDocument!.head!,
            location: frame.contentWindow!.location,
            history: frame.contentWindow!.history,
          };

          /*frameModeFrame下indexPath必定是字符串*/
          const indexPath = (await props.indexData.microConfig.index(props.tokenInfo)) as string;
          const publicPath = new URL(indexPath).pathname;
          const pathname = pathJoin('/' + publicPath + '/');

          /*克隆的节点*/
          const cloneElements = (() => {
            const documentElement = frameElements.document!.documentElement.cloneNode(true) as HTMLElement;
            const head = documentElement.querySelector('head')!;
            const body = documentElement.querySelector('body')!;
            body.style.position = 'relative';
            Object.entries({ head, body }).forEach(([elName, el]: [string, any]) => {
              ['appendChild', 'append', 'removeChild', 'remove',].forEach((methodName) => {
                const _method = el[methodName];
                el[methodName] = (child: HTMLElement) => {
                  // console.log({ el, methodName, child, });
                  if (child.tagName === 'SCRIPT') {
                    (frameElements as any)[elName][methodName](child);
                  } else if (child.tagName === 'STYLE') {
                    /*
                    * 应用挂载在shadow dom中的由于shadow dom中无法加载font-face，这
                    * 里加载style之后扫描出来里边的font-face，加载到主应用的head中
                    */
                    if (methodName === 'appendChild' || methodName === 'append') {
                      /*提前处理一遍*/
                      formatStyleTags({ ele: child as HTMLStyleElement, indexPath, pathname });
                    }
                    _method.apply(el, [child]);
                  } else if (child.tagName === 'LINK') {
                    const linkElement = child as HTMLLinkElement;
                    /*如果子应用的style资源以主应用的origin+子应用的pathname作为前缀，则视为不正确，替换为子应用的indexPath*/
                    const errorPrefix = pathJoin(window.location.origin, pathname);
                    let href = linkElement.href;
                    if (href.startsWith('http')) {
                      href = href.replace(errorPrefix, indexPath);
                    } else if (href.startsWith(pathname)) {
                      href = formatVitePublicPath(linkElement.href, pathname, indexPath);
                    }
                    if (linkElement.href !== href) {
                      linkElement.href = href;
                    }
                    // console.log({ href, new: linkElement.href, });
                    _method.apply(el, [linkElement]);
                  } else {
                    _method.apply(el, [child]);
                  }
                };
              });
            });
            /*因为esm代码中import css文件无法拦截，*/
            keepFormattingStyleTags({ el: head, pathname, indexPath });
            keepFormattingStyleTags({ el: body, pathname, indexPath });
            return {
              documentElement,
              head,
              body,
            };
          })();

          /*将克隆节点挂载到shadow dom中*/
          const shadowRoot = refs.el!.attachShadow({ mode: 'open' });
          shadowRoot.appendChild(cloneElements.documentElement);
          frame.style.display = 'none';

          /*代理劫持部分属性*/
          const delegateProps: (keyof typeof window.document)[] = [
            'body', 'head', 'querySelector', 'querySelectorAll',
            'addEventListener', 'removeEventListener', /*'activeElement',*/
            /*'createElement',*/ 'cloneNode', 'contains', 'documentElement',
            'dispatchEvent', 'getAnimations', 'getElementsByClassName', 'getElementsByTagName',
            'getElementsByTagNameNS', 'getElementsByName', 'getElementById', 'getRootNode',
            /*'hasFocus',*/ 'insertBefore', 'isEqualNode', 'isSameNode'
          ];
          delegateProps.forEach(propName => {
            Object.defineProperty(frameElements.window.document, propName, {
              get(): any {
                if (propName === 'head') {return cloneElements.head;}
                if (propName === 'body') {return cloneElements.body;}
                if (propName === 'documentElement') {return cloneElements.documentElement;}
                if (propName === 'getElementById') {return (id: string) => cloneElements.documentElement.querySelector(`#${id}`);}
                if (propName === 'getElementsByName') {return (name: string) => cloneElements.documentElement.querySelector(`[name=${name}]`);}
                const val = (cloneElements.documentElement as any)[propName];
                if (typeof val === "function") {
                  return val.bind(cloneElements.documentElement);
                } else {
                  return val;
                }
              }
            });
          });

          /*代理getComputedStyle，子应用获取shadow-dom节点的computedStyle时，直接返回子应用容器节点的样式*/
          /*Object.defineProperty(frameElements.window, 'getComputedStyle', {get() {return (el: any) => {if (el.nodeType === 11) {/!*document-fragment*!/return window.getComputedStyle(refs.el!);} else {return window.getComputedStyle(el);}};},});*/

          /*代理cloneElements.documentElement的parentNode以及parentElement，避免子应用通过 cloneElements.documentElement 找到父节点shadow dom*/
          /*子应用使用fixed定位实现的下拉框的话，比如element的popper.js，还需要写死 .el-popper 节点的position为absolute*/
          Object.defineProperty(cloneElements.documentElement, 'parentNode', { get(): any {return null;} });
          Object.defineProperty(cloneElements.documentElement, 'parentElement', { get(): any {return null;} })

          /*Object.defineProperty(frameElements.window.document, 'body', { get(): any {return cloneElements.body;} });
          Object.defineProperty(frameElements.window.document, 'head', { get(): any {return cloneElements.head;} });
          Object.defineProperty(frameElements.window.document, 'querySelector', { get(): any {return function (selector: string) {return cloneElements.documentElement.querySelector(selector);};} });
          Object.defineProperty(frameElements.window.document, 'querySelectorAll', { get(): any {return function (selector: string) {return cloneElements.documentElement.querySelectorAll(selector);};} });
          Object.defineProperty(frameElements.window.document, 'documentElement', { get(): any {return cloneElements.documentElement;} });*/

          /*劫持代理history的pushState以及replaceState，与主应用同步路由*/
          // eslint-disable-next-line @typescript-eslint/no-extra-semi
          ;['pushState', 'replaceState'].forEach(name => {
            const _old = (frameElements.history as any)[name]
            ;(frameElements.history as any)[name] = function (...args: any[]) {
              (window.history as any)[name](...args);
              _old.apply(frameElements.history, args);
            };
          });

          /**
           * 子应用可能会从documentElement中获取宽高尺寸来计算popup等下拉悬浮框的定位以及边界信息，这里将子应用的获取这些尺寸信息代理到主应用的documentElement上
           * @author  韦胜健
           * @date    2024/3/6 11:39
           */
          const delegateDocumentElementProps: (keyof HTMLHtmlElement)[] = ['scrollWidth', 'clientWidth', 'offsetWidth', "scrollHeight", 'clientHeight', 'offsetHeight'];
          delegateDocumentElementProps.forEach(propName => {Object.defineProperty(cloneElements.documentElement, propName, { get(): any {return (window.parent.document.documentElement as any)[propName];} });});

          console.log('frame.contentWindow.document', frame.contentWindow!.document.body, frame.contentWindow!.document.head);

          const appContainer = frame.contentDocument!.createElement('div') as HTMLDivElement;
          appContainer.setAttribute('id', 'app');
          appContainer.setAttribute('data-micro-root', props.indexData.microConfig.id);
          frame.contentDocument!.body.appendChild(appContainer);
          frame.contentDocument!.body.setAttribute('data-start-app', 'base');
          return { appContainer, Win: frame.contentWindow! };
        })();

        const globalCacheScript = getGlobalCacheScript(Win);
        const indexConfig = await execIndexScript({ indexData: props.indexData, Win, globalCacheScript, microApp });
        const indexConfigMainValue = await indexConfig.main();

        const mainConfig: iMicroMainConfig = await execMainScript({ indexConfigMainValue, indexData: props.indexData, Win, globalCacheScript });
        const subRoutePublicPath = microApp.route.buildMicroPublicPath({ microConfig: props.indexData.microConfig, subMicroPublicPath: mainConfig.getPublicPath() });

        microApp.defer.loadMicroInfoMap[props.indexData.microConfig.id].resolve({ subRoutePublicPath, Win, mainConfig });
        await microApp.defer.loadMicroReadyMap[props.indexData.microConfig.id].promise;

        const { renderMode } = props.indexData;
        if (renderMode === eRenderMode.base) {
          throw new Error(`子应用[${props.indexData.microConfig.id}]执行 mainConfig失败，当data-micro-frame为custom时，data-micro-render只能是custom，无法使用base`);
        } else {
          if ('custom' in mainConfig.frameMode) {
            mainConfig.frameMode.custom.init({ microConfig: props.indexData.microConfig, microApp, subRoutePublicPath, el: appContainer, dark: configuration.value.theme.dark, primary: configuration.value.theme.base.primary });
          } else {
            throw new Error("`子应用[${props.indexData.microConfig.id}]执行 mainConfig失败，当data-micro-render为custom时，子应用的mainConfig必须配置frameMode.custom`");
          }
        }
        onComplete();
      } catch (e: any) {
        const errMsg = `加载子应用[${props.indexData.microConfig.id}]失败，错误信息：` + (e.message || JSON.stringify(e));
        console.log(errMsg);
        $notice.error(errMsg);
        onComplete(errMsg);
        throw e;
      }
    });

    return () => (
      <div data-micro-container={props.indexData.microConfig.id} data-micro-frame-mode-frame={props.indexData.microConfig.id} ref={onRef.el}/>
    );
  },
});
